package com.sikaryofficial.gateway.filter;

import cn.hutool.core.collection.CollUtil;
import com.sikaryofficial.common.core.constant.GatewayFilterOrdered;
import com.sikaryofficial.common.core.utils.ServletUtils;
import com.sikaryofficial.common.core.utils.StringUtils;
import com.sikaryofficial.common.redis.service.RedisService;
import com.sikaryofficial.gateway.config.properties.SignCheckProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
 * 接口签名验证
 */
@Component
@Slf4j
public class SignCheckFilter implements GlobalFilter, Ordered {

    private static final String CLIENT_ID = "clientId";
    private static final String SIGN = "sign";
    private static final String TIMESTAMP = "timestamp";
    private static final String NONCE = "nonce";

    private static final String POST_BODY = "postBody";

    @Resource
    private SignCheckProperties signCheckProperties;

    @Resource
    private RedisService redisService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 是否要处理请求
        if (!signCheckProperties.isEnable()) {
            return chain.filter(exchange);
        }
        String path = exchange.getRequest().getURI().getPath();
        if (!isMatch(path)) {
            return chain.filter(exchange);
        }
        // 从url中获取签名相关参数：clientId， sign ， timestamp
        SignInfo signInfo = extractSignInfo(exchange);
        if (!signInfo.isValid()) {
            log.error("sign param error: {}", signInfo);
            return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "param error");
        }
        // 检验时间戳，是否在允许范围内
        if (!checkTimestamp(signInfo)) {
            log.error("timestamp is invalid. clientId:{},timestamp:{}", signInfo.getClientId(), signInfo.getTimestamp());
            return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "timestamp is invalid");
        }
        // 检验nonce，是否使用过
        Boolean notUsed = redisService.setIfAbsentCacheObject("hismk:sign:nonce:" + signInfo.getNonce(), signInfo.getNonce(), signCheckProperties.getTimeDiffSeconds() * 2L, TimeUnit.SECONDS);
        if (notUsed != null && !notUsed) {
            log.error("nonce has submitted. clientId:{},nonce:{}", signInfo.getClientId(), signInfo.getNonce());
            return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "nonce has submitted");
        }
        // 获取client对应的密钥
        String clientKey = getClientKey(signInfo);
        if (StringUtils.isNull(clientKey)) {
            log.error("clientKey is null. clientId:{}", signInfo.getClientId());
            return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "unknown client");
        }
        // 验证签名
        HttpMethod method = exchange.getRequest().getMethod();
        if (method == HttpMethod.GET) {
            return checkSignForGet(signInfo, clientKey, exchange, chain);
        } else if (method == HttpMethod.POST) {
            return checkSignForPost(signInfo, clientKey, exchange, chain);
        }
        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "method is not supported");
    }

    private Mono<Void> checkSignForGet(SignInfo signInfo, String clientKey, ServerWebExchange exchange, GatewayFilterChain chain) {
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
        String sign = createSign(queryParams, clientKey);
        if (!sign.equals(signInfo.getSign())) {
            log.error("sign is invalid. clientId:{},sign:{}", signInfo.getClientId(), signInfo.getSign());
            return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "sign is invalid");
        }
        return chain.filter(exchange);
    }

    private Mono<Void> checkSignForPost(SignInfo signInfo, String clientKey, ServerWebExchange exchange, GatewayFilterChain chain) {
        // post：直接获取原始的请求体信息来计算签名
        return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> serverHttpRequest.getBody().collectList() // 收集所有的 DataBuffer
                .flatMap(buffers -> {
                    byte[] bytes = new byte[buffers.stream().mapToInt(DataBuffer::readableByteCount).sum()];
                    int offset = 0;
                    for (DataBuffer buffer : buffers) {
                        buffer.read(bytes, offset, buffer.readableByteCount());
                        offset += buffer.readableByteCount();
                        DataBufferUtils.release(buffer); // 释放 buffer
                    }
                    String bodyStr = new String(bytes, StandardCharsets.UTF_8);
                    Map<String, List<String>> params = new HashMap<>(exchange.getRequest().getQueryParams());
                    params.put(POST_BODY, CollUtil.newArrayList(bodyStr));
                    String sign = createSign(params, clientKey);
                    if (!sign.equals(signInfo.getSign())) {
                        log.error("sign is invalid. clientId:{},sign:{}", signInfo.getClientId(), signInfo.getSign());
                        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "sign is invalid");
                    }
                    // don't mutate and build if same request object
                    if (serverHttpRequest == exchange.getRequest()) {
                        return chain.filter(exchange);
                    }
                    return chain.filter(exchange.mutate().request(serverHttpRequest).build());
                }));
    }

    private String createSign(Map<String, ?> requestParamMap, String clientKey) {
        requestParamMap = new TreeMap<>(requestParamMap);
        requestParamMap.remove(SIGN);

//        StringBuilder sb = new StringBuilder();
//        for (String key : requestParamMap.keySet()) {
//            Object value = requestParamMap.get(key);
//            if (value != null) {
//                sb.append(key).append("=").append(value).append("&");
//            }
//        }
//        sb.append(CLIENT_KEY).append("=").append(clientKey);
        String fullStr = requestParamMap + clientKey;
        String singStr = calculateMd5(fullStr);

        log.debug("fullStr:{}", fullStr);
        log.debug("signStr:{}", singStr);

        return singStr;
    }

    private String calculateMd5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(input.getBytes());
            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) {
                    hexString.append('0');
                }
                hexString.append(hex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            log.error("Failed to calculate MD5" + e.getMessage());
            throw new IllegalStateException("Error calculating MD5", e);
        }
    }

    /**
     * 获取client对应的密钥
     *
     * @param signInfo
     * @return
     */
    private String getClientKey(SignInfo signInfo) {
        String clientId = signInfo.getClientId();
        List<SignCheckProperties.SecretKey> secretKeys = signCheckProperties.getSecretKeys();
        if (CollUtil.isEmpty(secretKeys)) {
            return null;
        }
        for (SignCheckProperties.SecretKey secretKey : secretKeys) {
            if (secretKey.getClient().equals(clientId)) {
                return secretKey.getSecret();
            }
        }
        return null;
    }

    private boolean checkTimestamp(SignInfo signInfo) {
        Long timestamp = signInfo.getTimestamp();
        int diffMillis = signCheckProperties.getTimeDiffSeconds() * 1000;
        return Math.abs(System.currentTimeMillis() - timestamp) <= diffMillis;
    }


    private SignInfo extractSignInfo(ServerWebExchange exchange) {
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
        String clientId = queryParams.getFirst(CLIENT_ID);
        String sign = queryParams.getFirst(SIGN);
        String timestampStr = queryParams.getFirst(TIMESTAMP);
        String nonce = queryParams.getFirst(NONCE);

        Long timestamp = null;
        try {
            if (StringUtils.isNotEmpty(timestampStr)) {
                timestamp = Long.parseLong(timestampStr);
            }
        } catch (Exception e) {
            log.error("timestamp is invalid. timestampStr:{}", timestampStr);
        }

        SignInfo signInfo = new SignInfo();
        signInfo.setClientId(clientId);
        signInfo.setSign(sign);
        signInfo.setTimestamp(timestamp);
        signInfo.setNonce(nonce);
        return signInfo;
    }

    @Data
    public static class SignInfo {
        private String clientId;
        private String sign;
        private Long timestamp;
        private String nonce;

        public boolean isValid() {
            return StringUtils.isNotEmpty(clientId) && StringUtils.isNotEmpty(sign) && timestamp != null && StringUtils.isNotEmpty(nonce);
        }
    }

    private boolean isMatch(String path) {
        if (CollUtil.isEmpty(signCheckProperties.getIncludeUris())) {
            return false;
        }
        if (!StringUtils.matches(path, signCheckProperties.getIncludeUris())) {
            return false;
        }
        return CollUtil.isEmpty(signCheckProperties.getExcludeUris())
                || !StringUtils.matches(path, signCheckProperties.getExcludeUris());
    }

    @Override
    public int getOrder() {
        return GatewayFilterOrdered.REQUEST_SIGN;
    }
}
